/* * The MIT License (MIT) * * Copyright (c) 2014 Segment, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.segment.analytics; import android.util.JsonReader; import android.util.JsonToken; import android.util.JsonWriter; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Cartographer creates {@link Map} objects from JSON encoded streams and decodes {@link Map} * objects into JSON streams. Use {@link Builder} to construct instances. */ public class Cartographer { static final Cartographer INSTANCE = new Builder().lenient(true).prettyPrint(false).build(); private final boolean isLenient; private final boolean prettyPrint; Cartographer(boolean isLenient, boolean prettyPrint) { this.isLenient = isLenient; this.prettyPrint = prettyPrint; } /** * Deserializes the specified json into a {@link Map}. If you have the Json in a {@link Reader} * form instead of a {@link String}, use {@link #fromJson(Reader)} instead. */ public Map<String, Object> fromJson(String json) throws IOException { if (json == null) { throw new IllegalArgumentException("json == null"); } if (json.length() == 0) { throw new IllegalArgumentException("json empty"); } return fromJson(new StringReader(json)); } /** * Deserializes the json read from the specified {@link Reader} into a {@link Map}. If you have * the Json in a String form instead of a {@link Reader}, use {@link #fromJson(String)} instead. */ public Map<String, Object> fromJson(Reader reader) throws IOException { if (reader == null) { throw new IllegalArgumentException("reader == null"); } JsonReader jsonReader = new JsonReader(reader); jsonReader.setLenient(isLenient); try { return readerToMap(jsonReader); } finally { reader.close(); } } /** * Serializes the map into it's json representation and returns it as a String. If you want to * write the json to {@link Writer} instead of retrieving it as a String, use {@link #toJson(Map, * Writer)} instead. */ public String toJson(Map<?, ?> map) { StringWriter stringWriter = new StringWriter(); try { toJson(map, stringWriter); } catch (IOException e) { throw new AssertionError(e); // No I/O writing to a Buffer. } return stringWriter.toString(); } /** * Serializes the map into it's json representation into the provided {@link Writer}. If you want * to retrieve the json as a string, use {@link #toJson(Map)} instead. */ public void toJson(Map<?, ?> map, Writer writer) throws IOException { if (map == null) { throw new IllegalArgumentException("map == null"); } if (writer == null) { throw new IllegalArgumentException("writer == null"); } JsonWriter jsonWriter = new JsonWriter(writer); jsonWriter.setLenient(isLenient); if (prettyPrint) { jsonWriter.setIndent(" "); } try { mapToWriter(map, jsonWriter); } finally { jsonWriter.close(); } } // Decoding /** Reads the {@link JsonReader} into a {@link Map}. */ private static Map<String, Object> readerToMap(JsonReader reader) throws IOException { Map<String, Object> map = new LinkedHashMap<String, Object>(); reader.beginObject(); while (reader.hasNext()) { map.put(reader.nextName(), readValue(reader)); } reader.endObject(); return map; } /** Reads the {@link JsonReader} into a {@link List}. */ private static List<Object> readerToList(JsonReader reader) throws IOException { // todo: try to infer the type of the List? List<Object> list = new ArrayList<Object>(); reader.beginArray(); while (reader.hasNext()) { list.add(readValue(reader)); } reader.endArray(); return list; } /** Reads the next value in the {@link JsonReader}. */ private static Object readValue(JsonReader reader) throws IOException { JsonToken token = reader.peek(); switch (token) { case BEGIN_OBJECT: return readerToMap(reader); case BEGIN_ARRAY: return readerToList(reader); case BOOLEAN: return reader.nextBoolean(); case NULL: reader.nextNull(); // consume the null token return null; case NUMBER: return reader.nextDouble(); case STRING: return reader.nextString(); default: throw new IllegalStateException("Invalid token " + token); } } // Encoding /** Encode the given {@link Map} into the {@link JsonWriter}. */ private static void mapToWriter(Map<?, ?> map, JsonWriter writer) throws IOException { writer.beginObject(); for (Map.Entry<?, ?> entry : map.entrySet()) { writer.name(String.valueOf(entry.getKey())); writeValue(entry.getValue(), writer); } writer.endObject(); } /** Print the json representation of a List to the given writer. */ private static void listToWriter(List<?> list, JsonWriter writer) throws IOException { writer.beginArray(); for (Object value : list) { writeValue(value, writer); } writer.endArray(); } /** * Print the json representation of an array to the given writer. Primitive arrays cannot be cast * to Object[], to this method accepts the raw object and uses {@link Array#getLength(Object)} and * {@link Array#get(Object, int)} to read the array. */ private static void arrayToWriter(Object array, JsonWriter writer) throws IOException { writer.beginArray(); for (int i = 0, size = Array.getLength(array); i < size; i++) { writeValue(Array.get(array, i), writer); } writer.endArray(); } /** * Writes the given {@link Object} to the {@link JsonWriter}. * * @throws IOException */ private static void writeValue(Object value, JsonWriter writer) throws IOException { if (value == null) { writer.nullValue(); } else if (value instanceof Number) { writer.value((Number) value); } else if (value instanceof Boolean) { writer.value((Boolean) value); } else if (value instanceof List) { listToWriter((List) value, writer); } else if (value instanceof Map) { mapToWriter((Map) value, writer); } else if (value.getClass().isArray()) { arrayToWriter(value, writer); } else { writer.value(String.valueOf(value)); } } /** Fluent API to construct instances of {@link Cartographer}. */ public static class Builder { private boolean isLenient; private boolean prettyPrint; /** * Configure this parser to be be liberal in what it accepts. By default, this parser is strict * and only accepts JSON as specified by <a href="http://www.ietf.org/rfc/rfc4627.txt">RFC * 4627</a>. See {@link JsonReader#setLenient(boolean)} for more details. * </ul> */ public Builder lenient(boolean isLenient) { this.isLenient = isLenient; return this; } /** * Configures Cartographer to output Json that fits in a page for pretty printing. This option * only affects Json serialization. */ public Builder prettyPrint(boolean prettyPrint) { this.prettyPrint = prettyPrint; return this; } public Cartographer build() { return new Cartographer(isLenient, prettyPrint); } } }